package com.imis.base.shiro.authc;

import com.imis.base.constant.CommonConstant;
import com.imis.base.constant.enums.ArgumentResponseEnum;
import com.imis.base.constant.enums.CommonResponseEnum;
import com.imis.base.shiro.util.JwtUtil;
import com.imis.base.util.IPUtils;
import com.imis.base.util.RedisUtil;
import com.imis.base.util.SpringContextUtils;
import com.imis.module.system.model.converter.SysUserConverter;
import com.imis.module.system.model.po.SysUser;
import com.imis.module.system.model.vo.SysUserVO;
import com.imis.module.system.service.ISysUserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import java.util.Set;

/**
 * <p>
 * ShiroRealm<br>
 * 用户登录鉴权和获取用户授权
 * </p>
 *
 * @author XinLau
 * @version 1.0
 * @since 2019年03月26日 11:11
 */
@Component
@Slf4j
public class ShiroRealm extends AuthorizingRealm {

    /**
     * 系统用户表 服务类
     */
    @Lazy
    private ISysUserService sysUserService;

    @Autowired
    public void setSysUserService(ISysUserService sysUserService) {
        this.sysUserService = sysUserService;
    }

    /**
     * Redis 工具类
     */
    @Lazy
    private RedisUtil redisUtil;

    @Autowired
    public void setRedisUtil(RedisUtil redisUtil) {
        this.redisUtil = redisUtil;
    }

    /**
     * 必须重写此方法，不然Shiro会报错
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 功能： 获取用户权限信息，包括角色以及权限。只有当触发检测用户权限时才会调用此方法，例如checkRole,checkPermission
     *
     * @param principals token
     * @return AuthorizationInfo 权限信息
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        log.debug("————权限认证 [ roles、permissions]————");
        Long userId = null;
        if (principals != null) {
            // 1.获取当前请求用户信息
            SysUserVO userInfo = (SysUserVO) principals.getPrimaryPrincipal();
            userId = Long.valueOf(userInfo.getId());
        }
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 2.设置用户拥有的角色集合，比如“admin,test”
        Set<String> roleSet = sysUserService.queryUserRolesSetByUserId(userId);
        info.setRoles(roleSet);
        // 3.设置用户拥有的权限集合，比如“sys:role:add,sys:user:add”
        Set<String> permissionSet = sysUserService.queryUserPermissionsSetByUserId(userId);
        info.addStringPermissions(permissionSet);
        return info;
    }

    /**
     * 功能： 用来进行身份认证，也就是说验证用户输入的账号和密码是否正确，获取身份验证信息，错误抛出异常
     *
     * @param auth authenticationToken 用户身份信息 token
     * @return 返回封装了用户信息的 AuthenticationInfo 实例
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // 1.验证Token是否有值
        CommonResponseEnum.ERROR_NO_TOKEN.assertNotEmpty(token);
        log.debug("身份认证（验证） IP地址:  {}", IPUtils.getClientIpAddress(SpringContextUtils.getHttpServletRequest()));
        // 2.校验token有效性
        SysUserVO loginUser = this.checkUserTokenIsEffect(token);
        return new SimpleAuthenticationInfo(loginUser, token, getName());
    }

    /**
     * 校验token的有效性
     *
     * @param token -
     */
    public SysUserVO checkUserTokenIsEffect(String token) throws AuthenticationException {
        CommonResponseEnum.ERROR_NO_TOKEN.assertNotEmpty(token);
        // 解密获得 username，用于和数据库进行对比
        String username = JwtUtil.getUsername(token);
        CommonResponseEnum.ERROR_NO_TOKEN.assertNotEmpty(username);
        // 查询用户信息
        SysUser sysUser = sysUserService.queryUserByName(username);
        ArgumentResponseEnum.USER_LOGIN_ERR_NON.assertNotNull(sysUser);
        // 校验token是否超时失效 & 或者账号密码是否错误
        CommonResponseEnum.ERROR_NO_TOKEN.assertIsTrue(jwtTokenRefresh(token, username, sysUser.getPassword()));
        // 判断用户状态
        ArgumentResponseEnum.USER_LOGIN_ERR_FREEZE.assertIsTrue(CommonConstant.USER_UNFREEZE.equals(sysUser.getStatus()));
        return SysUserConverter.INSTANCE.getReturnValue(sysUser);
    }

    /**
     * JWTToken刷新生命周期 （解决用户一直在线操作，提供Token失效问题）
     * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)
     * 2、当该用户再次请求时，通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
     * 3、当该用户这次请求JWTToken值还在生命周期内，则会通过重新PUT的方式k、v都为Token值，缓存中的token值生命周期时间重新计算(这时候k、v值一样)
     * 4、当该用户这次请求jwt生成的token值已经超时，但该token对应cache中的k还是存在，则表示该用户一直在操作只是JWT的token失效了，程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值，该缓存生命周期重新计算
     * 5、当该用户这次请求jwt在生成的token值已经超时，并在cache中不存在对应的k，则表示该用户账户空闲超时，返回用户信息已失效，请重新登录。
     * 6、每次当返回为true情况下，都会给Response的Header中设置Authorization，该Authorization映射的v为cache对应的v值。
     * 7、注：当前端接收到Response的Header中的Authorization值会存储起来，作为以后请求token使用
     * 参考方案：https://blog.csdn.net/qq394829044/article/details/82763936
     *
     * @param token    -
     * @param userName -
     * @param passWord -
     * @return Boolean
     */
    public boolean jwtTokenRefresh(String token, String userName, String passWord) {
        // 1.获取Token缓存的Key
        String key = JwtUtil.getTokenKey(userName, passWord);
        // 2.获取Token缓存
        String cacheToken = String.valueOf(redisUtil.get(key));
        // 3.Token缓存空判断
        CommonResponseEnum.ERROR_NO_TOKEN.assertNotEmpty(cacheToken);
        // 4.Token 缓存比对
        CommonResponseEnum.ERROR_NO_TOKEN.assertIsTrue(cacheToken.equals(token));
        if (!JwtUtil.verify(cacheToken, userName, passWord)) {
            String newAuthorization = JwtUtil.sign(userName, passWord);
            // 5.1.设置新的缓存，重置超时时间
            redisUtil.set(key, newAuthorization, JwtUtil.EXPIRE_TIME);
            log.debug("——————————设置新的缓存，设置Token有效期保证不掉线—————————jwtTokenRefresh——————— {}", newAuthorization);
        } else {
            // 5.2.重置Token有效期
            redisUtil.set(key, cacheToken, JwtUtil.EXPIRE_TIME);
            log.debug("——————————用户在线操作，重置Token有效期保证不掉线—————————jwtTokenRefresh——————— {}", cacheToken);
        }
        return true;
    }

}
